package org.lisy.distributed.lock.redis;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

import com.google.common.collect.Lists;

import redis.clients.jedis.Jedis;

public class JedisLock {

	private static String LOCK_KEY = "lockKey";
	private static String REQUEST_ID_1 = "1";
	private static String REQUEST_ID_2 = "2";

	public static void main(String[] args) {
		Jedis jedis = RedisPool.getJedis();
		jedis.set("name", "lishangyuan");
		System.out.println(jedis.get("name"));
		
		boolean distributedLock = tryGetDistributedLock(jedis, LOCK_KEY, REQUEST_ID_1, 10*1000);
		if (distributedLock) {
			System.out.println("get lock: " + System.currentTimeMillis());
		} else {
			System.out.println("not get lock: " + System.currentTimeMillis());
			// releaseDistributedLock(jedis, LOCK_KEY, REQUEST_ID_1);
			// releaseDistributedLock(jedis, LOCK_KEY, REQUEST_ID_2);
			return;
		}
		while (true) {
			if (tryGetDistributedLock(jedis, LOCK_KEY, REQUEST_ID_2, 10*1000)) {
				System.out.println("lock expire: " + System.currentTimeMillis());
				break;
			}
			try {
				System.out.println("lock effective, wait 1s: " + System.currentTimeMillis());
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("test release lock: " + System.currentTimeMillis());
		distributedLock = tryGetDistributedLock(jedis, LOCK_KEY, REQUEST_ID_1, 10*1000);
		System.out.println("before release: " + distributedLock);
		releaseDistributedLock(jedis, LOCK_KEY, REQUEST_ID_2);
		distributedLock = tryGetDistributedLock(jedis, LOCK_KEY, REQUEST_ID_1, 10*1000);
		System.out.println("after release: " + distributedLock);
		RedisPool.returnResource(jedis);
		System.out.println("close RedisPool !");
	}

    /**
     * 尝试获取分布式锁
     * 
     * @param jedis Redis客户端
     * @param lockKey 锁标识
     * @param requestId 请求标识
     * @param expireTime 超期时间(ms)
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    	String flag = "right";
    	if ("err1".equals(flag)) {
    		/**
    		 * 错误演示一 
    		 * 缺点：非原子性操作不安全
    		 */
        	Long result = jedis.setnx(lockKey, requestId);
        	// 返回 1 表示获取到锁，0 表示未获取到锁
        	if (result == 1) {
        		// 若在这里程序突然崩溃，则无法设置过期时间，将发生死锁
        		jedis.expire(lockKey, 10);
        		return true;
        	}
    	} else if ("err2".equals(flag)) {
    		/**
    		 * 错误演示二 
    		 * 缺点：
    		 * 1. 由于是客户端自己生成过期时间，所以需要强制要求分布式下每个客户端的时间必须同步
    		 * 2. 当锁过期的时候，如果多个客户端同时执行 jedis.getSet()方法，那么虽然最终只有一个客户端可以加锁，但是这个客户端的锁的过期时间可能被其他客户端覆盖
    		 * 3. 锁不具备拥有者标识，即任何客户端都可以解锁
    		 */
    		long expires = System.currentTimeMillis() + expireTime;
    		String expiresStr = String.valueOf(expires);
    		// 如果当前锁不存在，返回加锁成功
    		if (jedis.setnx(lockKey, expiresStr) == 1) {
    			return true;
    		}
    		// 如果锁存在，获取锁的过期时间
    		String currentValueStr = jedis.get(lockKey);
    		if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
    			// 锁已过期，获取上一个锁的过期时间，并设置现在锁的过期时间
    			String oldValueStr = jedis.getSet(lockKey, expiresStr);
    			if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
    				// 考虑多线程并发的情况，只有一个线程的设置值和当前值相同，它才有权利加锁
    				return true;
    			}
    		}
    	} else {
    		/**
    		 * 正确演示
    		 * 参数解析：
             * 1.key:锁标识，保证唯一性
             * 2.value:请求标识，加锁解锁保持一致
             * 3.nxxx:传NX(SET IF NOT EXIST)，即当key不存在时，进行set操作；若key已经存在，则不做任何操作
             * 4.expx:传PX，给这个key加一个过期的设置，具体时间由time决定
             * 5.time:key的过期时间，单位ms
             */
            String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
            if ("OK".equals(result)) {
            	// 加锁成功, 启动一个延时线程, 防止业务逻辑未执行完毕就因锁超时而使锁释放
            	PostponeTask postponeTask = new PostponeTask(lockKey, requestId, expireTime);
            	Thread thread = new Thread(postponeTask);
            	thread.setDaemon(Boolean.TRUE);
            	thread.start();
                return true;
            }
    	}
        return false;
    }

    /**
     * 释放分布式锁
     * 
     * @param jedis Redis客户端
     * @param lockKey 锁标识
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
    	String flag = "right";
    	if ("err1".equals(flag)) {
    		/**
    		 * 错误演示一 
    		 * 缺点：非原子性操作不安全
    		 */
        	// 判断加锁与解锁是不是同一个客户端
    		if (requestId.equals(jedis.get(lockKey))) {
    			// 若在此时，这把锁突然不是这个客户端的，则会误解锁
    			jedis.del(lockKey);
    			return true;
    		}
    	} else {
    		/**
    		 * 正确演示
    		 * 
    		 * eval 命令执行 Lua 代码的时候，Lua 代码将被当成一个命令去执行，并且直到 eval 命令执行完成，Redis 才会执行其他命令
    		 */
            // Lua 代码解释：获取锁对应的 value 值，检查是否与 requestId 相等，如果相等则删除锁（解锁）
    		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            Long RELEASE_SUCCESS = 1L;
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
    	}
        return false;
    }

    /**
     * 锁延时
     * @param key 锁标识
     * @param value 请求标识
     * @param expireTime 锁超时时间(s)
     * @return
     */
    public static Boolean postpone(String key, String value, long expireTime) {
    	Jedis jedis = RedisPool.getJedis();
    	// 延时脚本
    	String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end";
        Object result = jedis.eval(script, Lists.newArrayList(key), Lists.newArrayList(value, String.valueOf(expireTime)));
        Long POSTPONE_SUCCESS = 1L;
        if (POSTPONE_SUCCESS.equals(result)) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
}
